﻿
package extremefx {
	import extremefx.conditions.requires;	
	import extremefx.conditions.requiresNumber;
	import extremefx.i18n.DateTimeFormatInfo;
	import extremefx.i18n.IFormatProvider;
	import extremefx.i18n.IFormattable;
	import extremefx.i18n.XDate;
	import extremefx.tools.DateUtils;		

	/**
	 * Represents an instant in time, typically expressed as a date and time of day.
	 * @author Marcelo Volmaro
	 */
	public final class DateTime implements IFormattable, IComparable, IEquatable, ICloneable {
		private static const _MAX_VALUE_TICKS:Number = Number.MAX_VALUE;
		private static const _DAYS_MONTH:Vector.<int> = Vector.<int>([0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]);
		private static const _DAYS_MONTHLEAP:Vector.<int> = Vector.<int>([0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]);
		
		/**
		 * Represents the largest possible value of DateTime. This field is read-only.
		 */
		public static const MAX_VALUE:DateTime = new DateTime(false, new TimeSpan(_MAX_VALUE_TICKS));
		
		/**
		 * Represents the smallest possible value of DateTime. This field is read-only.
		 */
		public static const MIN_VALUE:DateTime = new DateTime(false, new TimeSpan(0));

		internal var _timeSpan:TimeSpan;
		internal var _kind:uint;
		
		/**
		 * Initializes a new instance of the DateTime class.
		 * @param args [ticks|DateTime] | [yy, MM, dd] | [yy, MM, dd, hh, mm, ss] | [yy, MM, dd, hh, mm, ss, sss] | [yy, MM, dd, hh, mm, ss, sss, DateTimeKind]
		 */

		public function DateTime(...args){
			var kind:uint = DateTimeKind.UNSPECIFIED;
			var arg0:* = args[0];
			
			switch (args.length){
				case 1://ticks or DateTime
					var t:TimeSpan;
					
					if (arg0 is DateTime){
						t = new new TimeSpan((arg0 as DateTime)._timeSpan._ticks);
						kind = (arg0 as DateTime)._kind;
						
					} else if (arg0 is Date){
						t = new TimeSpan (_absoluteDays(arg0.fullYear, arg0.month+1, arg0.date), arg0.hours, arg0.minutes, arg0.seconds, arg0.milliseconds);
						
					} else {
						t = new TimeSpan (arg0 as Number);
					}
					
					requiresNumber(t.ticks, "Args").isInRange(MIN_VALUE.ticks, MAX_VALUE.ticks);
					_timeSpan = t;
				break;
				
				case 2://internal
					var val:TimeSpan = args[1] as TimeSpan;
					requiresNumber(val.ticks, "Args").isInRange(MIN_VALUE.ticks, MAX_VALUE.ticks);
					_timeSpan = val;
				break;
				
				case 3://yy, MM, dd
				case 6://yy, MM, dd, hh, mm, ss
				case 7://yy, MM, dd, hh, mm, ss, sss
				case 8://yy, MM, dd, hh, mm, ss, sss, DateTimeKind
					var _year:uint, _month:uint, _day:uint, _hour:uint, _min:uint, _sec:uint, _msec:uint;
					
					_year = arg0;
					_month = args[1];
					_day = args[2];
					_hour = args[3];
					_min = args[4];
					_sec = args[5];
					_msec = args[6];
					kind = args[7] || kind;
					
					if (_year < 1 || _year > 9999 || _month < 1 || _month >12  ||
					_day < 1 || _day > daysInMonth(_year, _month) || _hour < 0 || _hour > 23 ||
					_min < 0 || _min > 59 || _sec < 0 || _sec > 59 ||
					_msec < 0 || _msec > 999) throw new RangeError("Parameters describe an unrepresentable DateTime.");
					
					_timeSpan = new TimeSpan (_absoluteDays(_year,_month,_day), _hour, _min, _sec, _msec);
			}
			
			_kind = kind;
		}
		
		/**
		 * Gets the date component of this instance.
		 * @return A new DateTime with the same date as this instance, and the time value set to 12:00:00 midnight (00:00:00).
		 */
		public function get date():DateTime {
			var dt:DateTime = new DateTime(year, month, day);
			dt._kind = _kind;
			return dt;
		}
		
		/**
		 * Gets the number of ticks that represent the date and time of this instance.
		 * @return The number of ticks that represent the date and time of this instance. The value is between MIN_VALUE and MAX_VALUE.
		 */
		public function get ticks():Number {
			return _timeSpan._ticks;
		}
		
		/**
		 * Gets the year of the month represented by this instance.
		 * @return The year, between 1 and 9999.
		 */
		public function get year():int {
			return _fromTicks("year");
		}

		/**
		 * Gets the month of the month represented by this instance.
		 * @return The month component, expressed as a value between 1 and 12.
		 */
		public function get month():int {
			return _fromTicks("month");
		}
		
		/**
		 * Gets the day of the month represented by this instance.
		 * @return The day component, expressed as a value between 1 and 31.
		 */
		public function get day():int {
			return _fromTicks("day");
		}
		
		/**
		 * Gets the hour component of the date represented by this instance.
		 * @return The hour component, expressed as a value between 0 and 23.
		 */
		public function get hour():int {
			return _timeSpan.hours;
		}
		
		/**
		 * Gets the minute component of the date represented by this instance.
		 * @return The minute component, expressed as a value between 0 and 59.
		 */
		public function get minute():int {
			return _timeSpan.minutes;
		}
		
		/**
		 * Gets the second component of the date represented by this instance.
		 * @return The seconds, between 0 and 59.
		 */
		public function get second():int {
			return _timeSpan.seconds;
		}
		
		/**
		 * Gets the milliseconds component of the date represented by this instance.
		 * @return The milliseconds component, expressed as a value between 0 and 999.
		 */
		public function get millisecond():int {
			return _timeSpan.milliseconds;
		}
		
		/**
		 * Gets the day of the week represented by this instance.
		 * @return A DayOfWeek enumerated constant that indicates the day of the week. This property value ranges from zero, indicating Sunday, to six, indicating Saturday.
		 */
		public function get dayOfWeek():int {
			return (_timeSpan.days + 1) % 7;
		}
		
		/**
		 * Gets the day of the year represented by this instance.
		 * @return The day of the year, expressed as a value between 1 and 366.
		 */
		public function get dayOfYear():int {
			return _fromTicks("dayyear");
		}
		
		/**
		 * Gets the time of day for this instance.
		 * @return A TimeSpan that represents the fraction of the day that has elapsed since midnight.
		 */
		public function get timeOfDay():TimeSpan {
			return new TimeSpan(_timeSpan._ticks % TimeSpan.TICKS_PER_DAY);
		}
		
		/**
		 * Adds the specified number of ticks to the value of this instance.
		 * @param pO A number of 100-millisecond ticks. The value parameter can be positive or negative.
		 */
		public function addTicks(pO:Number):void {
			if ((pO + _timeSpan._ticks) > _MAX_VALUE_TICKS || (pO + _timeSpan._ticks) < 0) {
				throw new RangeError();
			}
			
			_timeSpan._ticks += pO;
		}
		
		
		/**
		 * Adds the value of the specified TimeSpan to the value of this instance.
		 * @param pO A TimeSpan that contains the interval to add.

		 */
		public function add(pO:TimeSpan):void {
			addTicks (pO._ticks);
		}
		
		/**
		 * Adds the specified number of milliseconds to the value of this instance.
		 * @param pO A number of whole and fractional milliseconds. The value parameter can be negative or positive.
		 */
		public function addMilliseconds(pO:Number):void {
			addTicks (pO);
		}
		
		/**
		 * Adds the specified number of seconds to the value of this instance.
		 * @param pO A number of whole and fractional seconds. The value parameter can be negative or positive.
		 */
		public function addSeconds(pO:Number):void {
			addMilliseconds(pO * 1000);
		}
		
		/**
		 * Adds the specified number of minutes to the value of this instance.
		 * @param pO A number of whole and fractional minutes. The value parameter can be negative or positive.
		 */
		public function addMinutes(pO:Number):void {
			addMilliseconds(pO * 60000);
		}
		
		/**
		 * Adds the specified number of hours to the value of this instance.
		 * @param pO A number of whole and fractional hours. The value parameter can be negative or positive.
		 */
		public function addHours(pO:Number):void {
			addMilliseconds(pO * 3600000);
		}
		
		/**
		 * Adds the specified number of days to the value of this instance.
		 * @param pO A number of whole and fractional days. The value parameter can be negative or positive.
		 */
		public function addDays(pO:Number):void {
			addMilliseconds(Math.round (pO * 86400000));
		}
		
		/**
		 * Adds the specified number of weeks to the value of this instance.
		 * @param pO A number of weeks. The value parameter can be negative or positive.
		 */
		public function addWeeks(pO:Number):void {
			addMilliseconds(Math.round (pO * 604800000));
		}
		
		
		/**
		 * Adds the specified number of months to the value of this instance.
		 * @param pO A number of months. The months parameter can be negative or positive.
		 */
		public function addMonths(pO:int):void {
			var _day:uint, _month:uint, _year:uint, _maxday:uint;
			var _temp:DateTime;

			_day = day;
			_month = month + (pO % 12);
			_year = year + pO/12 ;
			
			if (_month < 1) {
				_month += 12;
				_year--;
				
			} else if (_month>12) {
				_month -= 12;
				_year++;
			}
			
			_maxday = daysInMonth(_year, _month);
			if (_day > _maxday)
				_day = _maxday;

			_temp = new DateTime (_year, _month, _day);
			_temp.add(timeOfDay);
			_timeSpan = _temp._timeSpan;
		}

		/**
		 * Adds the specified number of years to the value of this instance.
		 * @param pO A number of years. The value parameter can be negative or positive.
		 */
		public function addYears(pO:int):void {
			addMonths(pO * 12);
		}
		
		/**
		 * Returns the number of days in the specified month and year.
		 * @param pYear The year.
		 * @param pMonth The month (a number ranging from 1 to 12).
		 * @return The number of days in month for the specified year. For example, if month equals 2 for February, the return value is 28 or 29 depending upon whether year is a leap year.
		 */
		public static function daysInMonth(pYear:int, pMonth:int):int {
			requiresNumber(pMonth, "pMonth").isInRange(1, 12);
			return (isLeapYear(pYear) ? _DAYS_MONTHLEAP  : _DAYS_MONTH)[pMonth];
		}
		
		/**
		 * Returns an indication whether the specified year is a leap year.
		 * @param pYear A 4-digit year.
		 * @return true if year is a leap year; otherwise, false.
		 */
		public static function isLeapYear(pYear:int):Boolean {
			return ((pYear % 4 == 0 && pYear % 100 != 0) || pYear % 400 == 0);
		}
		
		/**
		 * Indicates whether this instance of DateTime is within the Daylight Saving Time range for the current time zone.
		 * @return true if kind is Local or Unspecified and the value of this instance of DateTime is within the Daylight Saving Time range for the current time zone. false if kind is Utc.
		 */
		public function isDaylightSavingTime():Boolean {
			if (_kind == DateTimeKind.UTC) return false;
			
			return DateUtils.isDaylightSavingTime(new Date(year, month-1, day, hour, minute, second));
		}

		/**
		 * Converts the value of this instance to its equivalent long date string representation
		 * @return A string containing the name of the day of the week, the name of the month, the numeric day of the month, and the year equivalent to the date value of this instance.
		 */
		public function toLongDateString():String {
			return this.toString("D");
		}
		
		/**
		 * Converts the value of this instance to its equivalent long time string representation.
		 * @return A string containing the name of the day of the week, the name of the month, the numeric day of the hours, minutes, and seconds equivalent to the time value of this instance.
		 */
		public function toLongTimeString():String {
			return this.toString("T");
		}
		
		/**
		 * Converts the value of this instance to its equivalent short date string representation.
		 * @return A string containing the numeric month, the numeric day of the month, and the year equivalent to the date value of this instance.
		 */
		public function toShortDateString():String {
			return this.toString("d");
		}
		
		/**
		 * Converts the value of this instance to its equivalent short time string representation.
		 * @return A string containing the name of the day of the week, the name of the month, the numeric day of the hours, minutes, and seconds equivalent to the time value of this instance.
		 */
		public function toShortTimeString():String {
			return this.toString("t");
		}
		
		/**
		 * Gets a value that indicates whether the time represented by this instance is based on local time, Coordinated Universal Time (UTC), or neither.
		 * @return One of the DateTimeKind values. The default is unspecified.
		 */
		public function get kind():uint {
			return _kind;
		}
		
		/**
		 * Returns a DateTime object that is set to the current date and time on this computer, expressed as the local time.
		 * @return A DateTime whose value is the current local date and time.
		 */
		public static function get now():DateTime {
			var dt:DateTime = new DateTime(new Date());
			dt._kind = DateTimeKind.LOCAL;
			return dt;
		}
		
		/**
		 * Gets a DateTime object that is set to the current date and time on this computer, expressed as the Coordinated Universal Time (UTC).
		 * @return A DateTime whose value is the current UTC date and time.
		 */
		public static function get utcNow():DateTime {
			var now:Date = new Date();
			var dt:DateTime = new DateTime(now.getUTCFullYear(), now.getUTCMonth() + 1, now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds(), now.getUTCMilliseconds());
			dt._kind = DateTimeKind.UTC;
			return dt;
		}


		/**
		 * Gets the current date
		 * @return A DateTime set to today's date, with the time component set to 00:00:00.
		 */
		public static function get today():DateTime {
			var now:Date = new Date();
			var dt:DateTime = new DateTime(now.getFullYear(), now.getMonth() + 1, now.getDate(), 0, 0, 0);
			dt._kind = DateTimeKind.LOCAL;
			return dt;
		}
		
		/**
		 * Subtracts the specified duration from this instance
		 * @param pT An instance of TimeSpan
		 * @return A DateTime equal to the date and time represented by this instance minus the time interval represented by pT.
		 */
		public function subtractTimeSpan(pT:TimeSpan):DateTime {
			var dt:DateTime = new DateTime(true, new TimeSpan (_timeSpan._ticks - pT._ticks));
			dt._kind = _kind;
			return dt;
		}
		
		/**
		 * Subtracts the specified date and time from this instance
		 * @param pD An instance of DateTime.
		 * @return A TimeSpan interval equal to the date and time represented by this instance minus the date and time represented by pD.
		 */
		public function subtractDateTime(pD:DateTime):TimeSpan {
			return new TimeSpan(_timeSpan._ticks - pD._timeSpan._ticks);
		}
		
		/**
		 * Converts the value of this instance to its equivalent string representation using the specified format and culture-specific format information.
		 * @param pFormat: A format string.
		 * @param pFProvider: An IFormatProvider that supplies culture-specific formatting information.
 		 */
		
		public function toString(pFormat:String = "", pFProvider:IFormatProvider = null):String {
			if (pFormat == "") pFormat = "G";
			var d:Date = new Date(year, month-1, day, hour, minute, second, millisecond);
			return XDate.toString(d, pFormat, pFProvider);
		}
		
		/**
		 * Converts the specified string representation of a date and time to its DateTime equivalent using the specified culture-specific format information.
		 * @param pVal A string containing a date and time to convert.
		 * @param pFmtProv An optional IFormatProvider that supplies culture-specific format information about pVal.
		 * @return A DateTime equivalent to the date and time contained in s as specified by provider.
		 */
		public static function parse(pVal:String, pFmtProv:IFormatProvider = null):DateTime {
			var d:Date = XDate.parse(pVal, pFmtProv);
			return new DateTime(d.fullYear, d.month+1, d.date, d.hours, d.minutes, d.seconds, d.milliseconds);
		}
		
		/**
		 * Converts the specified string representation of a date and time to its DateTime equivalent using the specified format and culture-specific format information. The format of the string representation must match the specified format exactly.
		 * @param pVal A string containing a date and time to convert.
		 * @param pFormats A string / an array of expected formats of pVal.
		 * @param pFmtProv An IFormatProvider that supplies culture-specific format information about pVal.
		 */
		public static function parseExact(pVal:String, pFormats:*, pFmtProv:IFormatProvider):DateTime {
			var d:Date = XDate.parseExact(pVal, pFormats, DateTimeFormatInfo.getInstance (pFmtProv));
			return new DateTime(d.fullYear, d.month+1, d.date, d.hours, d.minutes, d.seconds, d.milliseconds);
		}
		
		private function _fromTicks(pFrom:String):uint {
			var num400:uint, num100:uint, num4:uint, numyears:uint;
			var M:uint = 1;

//			var days:Vector.<int> = _DAYS_MONTH;
			var totaldays:uint = _timeSpan.days;

			num400 = totaldays / 146097;
			totaldays -=  (num400 * 146097);
		
			num100 = totaldays / 36524;
			
			if (num100 == 4) num100 = 3;
			totaldays -= (num100 * 36524);

			num4 = totaldays / 1461;
			totaldays -= (num4 * 1461);

			numyears = totaldays / 365;
			
			if (numyears == 4) numyears =3;
			if (pFrom == "year") return num400*400 + num100*100 + num4*4 + numyears + 1;

			totaldays -= (numyears * 365);
			
			if (pFrom == "dayyear") return totaldays + 1;
//			if ((numyears==3) && ((num100 == 3) || !(num4 == 24))) days = _DAYS_MONTHLEAP;
			
//			while (totaldays >= days[M]) totaldays -= days[M++];

			if (pFrom == "month") return M;

			return totaldays+1;
		}
		
		private function _absoluteDays(pYear:uint, pMonth:uint, pDay:uint):uint {
			var temp:uint = 0;
			var m:uint = 1;
		
//			var days:Vector.<int> = (isLeapYear(pYear) ? _DAYS_MONTHLEAP  : _DAYS_MONTH);
			
			while (m < pMonth) {
//				temp += days[m++];
			}
			pYear--;
			
			return ((pDay-1) + temp + (365 * pYear) + (pYear/4) - (pYear/100) + (pYear/400));
		}
		
		/**
		 * Compares two instances of DateTime and returns an indication of their relative values.
		 * @param pT1 The first DateTime.
		 * @param pT2 The second DateTime.
		 * @return A signed int indicating the relative values of t1 and t2
		 */
		public static function compare(pT1:DateTime, pT2:DateTime):int {
			if (pT1._timeSpan._ticks < pT2._timeSpan._ticks){
				return -1;
				
			} else if (pT1._timeSpan._ticks > pT2._timeSpan._ticks){
				return 1;
				
			} else {
				return 0;
			}
		}
		
		/**
		 * Compares this instance to a specified object or value type and returns an indication of their relative values.
		 * @param pObj A DateTime object to compare.
		 * @return A signed number indicating the relative values of this instance and the pObj parameter.
		 */
		public function compareTo(pObj:*):int {
			if (pObj == null) return 1;
			
			requires(pObj, "pObj").isOfType(DateTime);
			return compare(this, DateTime(pObj));
		}
		
		public function equals(pObj:*):Boolean {
			if (!(pObj is DateTime)) return false;
			return (pObj as DateTime)._timeSpan._ticks == _timeSpan._ticks;
		}
		
		public function clone():Object {
			return new DateTime(this);
		}		
	}
}
